/**
 * Copyright 2019 吉鼎科技.

 * <p>
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.easyplatform.web.ext.calendar;

import cn.easyplatform.web.ext.Queryable;
import cn.easyplatform.web.ext.Widget;
import cn.easyplatform.web.ext.ZkExt;
import cn.easyplatform.web.ext.calendar.api.CalendarEvent;
import cn.easyplatform.web.ext.calendar.api.CalendarModel;
import cn.easyplatform.web.ext.calendar.api.DateFormatter;
import cn.easyplatform.web.ext.calendar.api.RenderContext;
import cn.easyplatform.web.ext.calendar.event.*;
import cn.easyplatform.web.ext.calendar.impl.SimpleCalendarEvent;
import cn.easyplatform.web.ext.calendar.impl.SimpleCalendarModel;
import cn.easyplatform.web.ext.calendar.impl.Util;
import org.apache.commons.lang3.time.DateFormatUtils;
import org.zkoss.lang.Classes;
import org.zkoss.lang.Objects;
import org.zkoss.util.Locales;
import org.zkoss.util.TimeZones;
import org.zkoss.zk.au.out.AuSetAttribute;
import org.zkoss.zk.ui.Component;
import org.zkoss.zk.ui.UiException;
import org.zkoss.zk.ui.event.Events;
import org.zkoss.zk.ui.ext.Disable;
import org.zkoss.zk.ui.sys.ContentRenderer;
import org.zkoss.zk.ui.sys.DesktopCtrl;
import org.zkoss.zul.Toolbar;
import org.zkoss.zul.impl.XulElement;

import java.io.IOException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.*;
import java.util.Map.Entry;

/**
 * @author <a href="mailto:davidchen@epclouds.com">littleDog</a> <br/>
 * @since 2.0.0 <br/>
 */
public class Calendars extends XulElement implements ZkExt, Disable, Queryable {

    private static final long serialVersionUID = 20090331171730L;

    private static final String ATTR_ON_INIT_POSTED = "cn.easyplatform.web.ext.calendar.Calendars.onInitLaterPosted";
    private static final String ATTR_ON_ADD_EVENT_RESPONSE = "cn.easyplatform.web.ext.calendar.Calendars.onAddEventResponse";
    private static final String ATTR_ON_REMOVE_EVENT_RESPONSE = "cn.easyplatform.web.ext.calendar.Calendars.onRemoveEventResponse";
    private static final String ATTR_ON_MODIFY_EVENT_RESPONSE = "cn.easyplatform.web.ext.calendar.Calendars.onModifyEventResponse";

    private int _firstDayOfWeek = 2;
    private Date _curDate;
    private int _days = 7;
    private DateFormatter _dfmter;
    private Map<String, List<CalendarEvent>> _evts;
    private Map<TimeZone, String> _tzones;
    private Map<Object, Object> _ids;
    private CalendarModel _model;
    private transient CalendarDataListener _dataListener;
    private SimpleDateFormat _sdfKey = new SimpleDateFormat("yyyy-MM-dd");
    private boolean _readonly;
    private boolean _weekOfYear;
    private boolean _hasEmptyZone = false;
    private boolean _escapeXML = true;
    private List<CalendarEvent> _addEvtList, _mdyEvtList, _rmEvtList;
    private int _beginTime = 0, _endTime = 24, _timeslots = 2;

    private static final Comparator<CalendarEvent> _defCompare = new Comparator<CalendarEvent>() {
        public int compare(CalendarEvent o1, CalendarEvent o2) {
            return o1.getBeginDate().compareTo(o2.getBeginDate());
        }
    };

    static {
        addClientEvent(Calendars.class, CalendarsEvent.ON_EVENT_CREATE, 0);
        addClientEvent(Calendars.class, CalendarsEvent.ON_EVENT_EDIT, 0);
        addClientEvent(Calendars.class, CalendarsEvent.ON_EVENT_UPDATE, 0);
        addClientEvent(Calendars.class, CalendarsEvent.ON_DAY_CLICK,
                CE_REPEAT_IGNORE);
        addClientEvent(Calendars.class, CalendarsEvent.ON_WEEK_CLICK,
                CE_REPEAT_IGNORE);
        // since 2.1.5
        addClientEvent(Calendars.class, CalendarsEvent.ON_EVENT_TOOLTIP,
                CE_REPEAT_IGNORE);
    }

    /**
     * 查询语句 select x1 as id,x2 as title,x3 as beginDate,x4 as endDate,x5 as
     * content,x6 as headerColor,x7 as contentColor,x8 as locked
     */
    private Object _query;

    /**
     * 数据连接的资源id
     */
    private String _dbId;

    /**
     * 过滤表达式一定存在
     */
    private String _filter;

    /**
     * 拖放事件
     */
    private String _dndEvent;

    /**
     * 创建事件
     */
    private String _createEvent;

    /**
     * 编辑事件
     */
    private String _editEvent;

    /**
     *
     */
    private String _dayEvent;

    /**
     *
     */
    private String _weekEvent;

    /**
     * 提示事件
     */
    private String _tooltipEvent;


    public Calendars() {
        init();
        _curDate = getCalendar().getTime();
    }

    public String getTooltipEvent() {
        return _tooltipEvent;
    }

    public void setTooltipEvent(String tooltipEvent) {
        this._tooltipEvent = tooltipEvent;
    }

    /**
     * @return the dndEvent
     */
    public String getDndEvent() {
        return _dndEvent;
    }

    /**
     * @param dndEvent the dndEvent to set
     */
    public void setDndEvent(String dndEvent) {
        this._dndEvent = dndEvent;
    }

    /**
     * @return the createEvent
     */
    public String getCreateEvent() {
        return _createEvent;
    }

    /**
     * @param createEvent the createEvent to set
     */
    public void setCreateEvent(String createEvent) {
        this._createEvent = createEvent;
    }

    /**
     * @return the editEvent
     */
    public String getEditEvent() {
        return _editEvent;
    }

    /**
     * @param editEvent the editEvent to set
     */
    public void setEditEvent(String editEvent) {
        this._editEvent = editEvent;
    }

    /**
     * @return the dayEvent
     */
    public String getDayEvent() {
        return _dayEvent;
    }

    /**
     * @param dayEvent the dayEvent to set
     */
    public void setDayEvent(String dayEvent) {
        this._dayEvent = dayEvent;
    }

    /**
     * @return the weekEvent
     */
    public String getWeekEvent() {
        return _weekEvent;
    }

    /**
     * @param weekEvent the weekEvent to set
     */
    public void setWeekEvent(String weekEvent) {
        this._weekEvent = weekEvent;
    }

    private void init() {
        _evts = new HashMap<String, List<CalendarEvent>>(32);
        _tzones = new LinkedHashMap<TimeZone, String>();
        _ids = new HashMap<Object, Object>(32);
        _firstDayOfWeek = getCalendar().getFirstDayOfWeek();
    }

    /**
     * Sets whether enable to show the week number within the current year or
     * not.
     */
    public void setWeekOfYear(boolean weekOfYear) {
        if (_weekOfYear != weekOfYear) {
            _weekOfYear = weekOfYear;
            if (!"month".equals(getMold()))
                return;

            TimeZone timezone = getDefaultTimeZone();
            Calendar cal = Calendar.getInstance(timezone);
            cal.setTime(getBeginDate());

            smartUpdate("woy", String.valueOf(cal.get(Calendar.WEEK_OF_YEAR)));
        }
    }

    /**
     * Returns whether enable to show the week number within the current year or
     * not.
     * <p>
     * Default: false
     */
    public boolean isWeekOfYear() {
        return _weekOfYear;
    }

    /**
     * Sets whether it is readonly.
     */
    public void setReadonly(boolean readonly) {
        if (_readonly != readonly) {
            _readonly = readonly;
            smartUpdate("readonly", _readonly);
        }
    }

    /**
     * Returns whether it is readonly.
     * <p>
     * Default: false.
     */
    public boolean isReadonly() {
        return _readonly;
    }

    /**
     * Sets the date formatter. In fact, there are five places in the calendar
     * must have different date display.
     *
     * @see DateFormatter
     */
    public void setDateFormatter(DateFormatter dfmater) {
        if (_dfmter != dfmater) {
            _dfmter = dfmater;
            reSendDateRange();
            smartUpdate("cd", Util.getDSTTime(this.getDefaultTimeZone(),
                    getCurrentDate()));
            reSendEventGroup();
        }
    }

    /**
     * Sets the date formatter by a class name.
     *
     * @see DateFormatter
     * @see #setDateFormatter(DateFormatter)
     */
    public void setDateFormatter(String clsnm) throws ClassNotFoundException,
            NoSuchMethodException, IllegalAccessException,
            InstantiationException, java.lang.reflect.InvocationTargetException {
        if (clsnm != null)
            setDateFormatter((DateFormatter) Classes.newInstanceByThread(clsnm));
    }

    /**
     * Returns the date formatter.
     * <p>
     * Note: never null.
     */
    public DateFormatter getDateFormatter() {
        return _dfmter;
    }

    /**
     * Sets the days, that is, how many column should be displayed on the
     * default mold.
     * <p>
     * Default: 7. (i.e. one week), in month view, the attribute will be
     * ignored.
     */
    public void setDays(int days) {
        if (days <= 0)
            days = 1;
        if (days != _days) {
            _days = days;
            if (!inMonthMold()) {
                reSendDateRange();
                smartUpdate("days", _days);
                reSendEventGroup();
            }
        }
    }

    /**
     * Returns the days.
     * <p>
     * Default: 7
     */
    public int getDays() {
        return _days;
    }

    /**
     * Sets what the first day of the week is; e.g., <code>SUNDAY</code> in the
     * U.S., <code>MONDAY</code> in France.
     * <p>
     * Default: {@link Calendar#SUNDAY}
     * <p>
     * Note: it is only allowed when days with 7 in the default mold or using
     * the month mold.
     *
     * @param value the given first day of the week.
     * @see #getFirstDayOfWeek()
     * @see java.util.Calendar#setFirstDayOfWeek
     */
    public void setFirstDayOfWeek(int value) {
        if (_firstDayOfWeek != value) {
            _firstDayOfWeek = value;
            if (_days == 7 || inMonthMold()) {
                reSendDateRange();
                smartUpdate("firstDayOfWeek", _firstDayOfWeek);
                reSendEventGroup();
            }
        }
    }

    /**
     * Sets what the first day of the week is.
     * <p>
     * Note: it is only allowed when days with 7 in the default mold or using
     * the month mold.
     *
     * @param day <code>SUNDAY</code>, <code>MONDAY</code>, <code>TUESDAY</code>
     *            , <code>WEDNESDAY</code>, <code>THURSDAY</code>,
     *            <code>FRIDAY</code>, and <code>SATURDAY</code>. Case
     *            insensitive
     */
    public void setFirstDayOfWeek(String day) {
        if ("SUNDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(1);
        else if ("MONDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(2);
        else if ("TUESDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(3);
        else if ("WEDNESDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(4);
        else if ("THURSDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(5);
        else if ("FRIDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(6);
        else if ("SATURDAY".equalsIgnoreCase(day))
            setFirstDayOfWeek(7);
    }

    /**
     * Gets what the first day of the week is; e.g., <code>SUNDAY</code> in the
     * U.S., <code>MONDAY</code> in France.
     * <p>
     * Default: {@link Calendar#SUNDAY}
     *
     * @return the first day of the week.
     * @see #setFirstDayOfWeek(int)
     * @see java.util.Calendar#getFirstDayOfWeek
     */
    public int getFirstDayOfWeek() {
        return _firstDayOfWeek;
    }

    public void onAddDayEventResponse() {
        removeAttribute(ATTR_ON_ADD_EVENT_RESPONSE);
        response("addEvent" + getUuid(), new AuSetAttribute(this,
                "addDayEvent", Util.encloseEventList(this, _addEvtList)));
    }

    public void onRemoveDayEventResponse() {
        removeAttribute(ATTR_ON_REMOVE_EVENT_RESPONSE);
        response("removeEvent" + getUuid(), new AuSetAttribute(this,
                "removeDayEvent", Util.encloseEventList(this, _rmEvtList)));
    }

    public void onModifyDayEventResponse() {
        removeAttribute(ATTR_ON_MODIFY_EVENT_RESPONSE);
        response("modifyEvent" + getUuid(), new AuSetAttribute(this,
                "modifyDayEvent", Util.encloseEventList(this, _mdyEvtList)));
    }

    public void addEvent(KalendarEvent event) {
        for (Date date : event.getDate()) {
            SimpleCalendarEvent ce = new SimpleCalendarEvent();
            ce.setId(DateFormatUtils.format(date, "yyyyMMdd"));
            ce.setTitle(event.getTitle());
            ce.setContent(event.getTitle());
            ce.setBeginDate(date);
            Calendar calendar = Calendar.getInstance();
            calendar.setTime(date);
            calendar.add(Calendar.DAY_OF_YEAR, 1);
            ce.setEndDate(calendar.getTime());
            ce.setContentColor(event.getColor());
            ce.setHeaderColor(event.getColor());
            ((SimpleCalendarModel) _model).add(ce);
        }
    }

    public void removeEvent(KalendarEvent event) {
        String id = DateFormatUtils.format(event.getDate()[0], "yyyyMMdd");
        SimpleCalendarEvent ce = (SimpleCalendarEvent) _model
                .getEventById(id);
        if (ce != null)
            ((SimpleCalendarModel) _model).remove(ce);
    }

    public void addEvent(Map<String, Object> event) {
        SimpleCalendarEvent ce = new SimpleCalendarEvent();
        for (Map.Entry<String, Object> entry : event.entrySet()) {
            if (entry.getKey().equalsIgnoreCase("id"))
                ce.setId(entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("begindate"))
                ce.setBeginDate((Date) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("enddate"))
                ce.setEndDate((Date) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("title"))
                ce.setTitle((String) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("content"))
                ce.setContent((String) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("headerColor"))
                ce.setHeaderColor((String) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("contentColor"))
                ce.setContentColor((String) entry.getValue());
            else if (entry.getKey().equalsIgnoreCase("locked")) {
                String val = entry.getValue().toString();
                ce.setLocked(val.equals("1") || val.equalsIgnoreCase("true")
                        || val.equals("T"));
            }
        }
        ((SimpleCalendarModel) _model).add(ce);
    }

    public void updateEvent(Map<String, Object> event) {
        Object id = null;
        for (Map.Entry<String, Object> entry : event.entrySet()) {
            if (entry.getKey().equalsIgnoreCase("id")) {
                id = entry.getValue();
                break;
            }
        }
        SimpleCalendarEvent ce = (SimpleCalendarEvent) _model.getEventById(id);
        if (ce != null) {
            for (Map.Entry<String, Object> entry : event.entrySet()) {
                if (entry.getKey().equalsIgnoreCase("begindate"))
                    ce.setBeginDate((Date) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("enddate"))
                    ce.setEndDate((Date) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("title"))
                    ce.setTitle((String) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("content"))
                    ce.setContent((String) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("headerColor"))
                    ce.setHeaderColor((String) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("contentColor"))
                    ce.setContentColor((String) entry.getValue());
                else if (entry.getKey().equalsIgnoreCase("locked")) {
                    String val = entry.getValue().toString();
                    ce.setLocked(val.equals("1")
                            || val.equalsIgnoreCase("true") || val.equals("T"));
                }
            }
            ((SimpleCalendarModel) _model).update(ce);
        }
    }

    public void removeEvent(Object id) {
        SimpleCalendarEvent ce = (SimpleCalendarEvent) _model.getEventById(id);
        if (ce != null)
            ((SimpleCalendarModel) _model).remove(ce);
    }

    public void addDayEvent(CalendarEvent ce) {
        if (ce == null)
            return;
        if (_addEvtList == null)
            _addEvtList = new LinkedList<CalendarEvent>();
        _addEvtList.add(ce);
        if (getAttribute(ATTR_ON_ADD_EVENT_RESPONSE) == null) {
            setAttribute(ATTR_ON_ADD_EVENT_RESPONSE, Boolean.TRUE);
            Events.postEvent(-20000, "onAddDayEventResponse", this, null);
        }
    }

    public void modifyDayEvent(CalendarEvent ce) {
        if (ce == null)
            return;
        if (_mdyEvtList == null)
            _mdyEvtList = new LinkedList<CalendarEvent>();
        _mdyEvtList.add(ce);
        if (getAttribute(ATTR_ON_MODIFY_EVENT_RESPONSE) == null) {
            setAttribute(ATTR_ON_MODIFY_EVENT_RESPONSE, Boolean.TRUE);
            Events.postEvent(-20000, "onModifyDayEventResponse", this, null);
        }
    }

    public void removeDayEvent(CalendarEvent ce) {
        if (ce == null)
            return;
        if (_rmEvtList == null)
            _rmEvtList = new LinkedList<CalendarEvent>();
        _rmEvtList.add(ce);
        if (getAttribute(ATTR_ON_REMOVE_EVENT_RESPONSE) == null) {
            setAttribute(ATTR_ON_REMOVE_EVENT_RESPONSE, Boolean.TRUE);
            Events.postEvent(-20000, "onRemoveDayEventResponse", this, null);
        }
    }

    private void cleanEmptyZone() {
        Map<TimeZone, String> tzones = new HashMap<TimeZone, String>(_tzones);

        for (Iterator<Entry<TimeZone, String>> it = tzones.entrySet()
                .iterator(); it.hasNext(); ) {
            Entry<TimeZone, String> es = it.next();
            if (es.getValue().equals(""))
                _tzones.remove(es.getKey());
        }
        _hasEmptyZone = false;
    }

    private Calendar getCalendar() {
        return Calendar.getInstance(getDefaultTimeZone(), Locales.getCurrent());
    }

    /**
     * Adds the time zone to the calendar.
     * <p>
     * Note: the first added will be the default time zone of the calendar.
     *
     * @param label    the description of the time zone.
     * @param timezone a time zone. (Cannot duplicate)
     */
    public void addTimeZone(String label, TimeZone timezone) {
        if (label == null)
            label = "";
        _tzones.put(timezone, label);
        if (_hasEmptyZone)
            cleanEmptyZone();
        if (!inMonthMold() && _dfmter != null) {
            Calendar cal = getCalendar();
            cal.set(Calendar.MINUTE, 0);
            smartUpdate("captionByTimeOfDay", Util.encloseList(Util
                    .packCaptionByTimeOfDay(cal, _tzones, Locales.getCurrent(),
                            _dfmter)));
        }

        TimeZone tz = getDefaultTimeZone();
        smartUpdate("tz", (tz.getRawOffset()) / 60000);
        smartUpdate("bd", Util.getDSTTime(tz, getBeginDate()));
        smartUpdate("ed", Util.getDSTTime(tz, getEndDate()));
        reSendEventGroup();
    }

    /**
     * Adds the time zone to the calendar.
     * <p>
     * Note: the first added will be the default time zone of the calendar.
     *
     * @param label    the description of the time zone.
     * @param timezone a id of time zone. (Cannot duplicate)
     * @see TimeZone#getTimeZone(String)
     * @see #addTimeZone(String, TimeZone)
     */
    public void addTimeZone(String label, String timezone) {
        addTimeZone(label, TimeZone.getTimeZone(timezone));
    }

    /**
     * Sets the time zone to the calendar, it is easily used for ZUL file. e.g.
     * <p>
     * <pre>
     * &lt;calendars timeZone=&quot;Taiwan=GMT+8, Sweden=GMT+1,&quot;&gt;
     * </pre>
     *
     * @param timezone
     */
    public void setTimeZone(String timezone) {
        if (timezone == null)
            throw new IllegalArgumentException("The timezone is null!");

        String[] ary = timezone.trim().split(",");
        for (int i = 0, j = ary.length; i < j; i++) {
            String timezoneString = ary[i];
            String[] pair = timezoneString.split("=");
            addTimeZone(pair[0].trim(), pair[1].trim());
        }
    }

    /**
     * Removes the time zone from the calendar
     */
    public boolean removeTimeZone(TimeZone timezone) {
        if (_tzones.remove(timezone) != null) {
            TimeZone tz = getDefaultTimeZone();
            smartUpdate("bd", Util.getDSTTime(tz, getBeginDate()));
            smartUpdate("ed", Util.getDSTTime(tz, getEndDate()));
            reSendEventGroup();
            return true;
        }
        return false;
    }

    /**
     * Returns the current time zone of the calendar.
     */
    public TimeZone getDefaultTimeZone() {
        if (_tzones.isEmpty()) {
            _hasEmptyZone = true;
            TimeZone t = TimeZones.getCurrent();
            _tzones.put(t, "");
            return t;
        }
        return (TimeZone) _tzones.keySet().iterator().next();
    }

    /**
     * Returns the unmodifiable map including all the timezone inside the
     * calendar.
     */
    public Map<TimeZone, String> getTimeZones() {
        return Collections.unmodifiableMap(_tzones);
    }

    public String getCalendarEventId(CalendarEvent ce) {
        Object o = _ids.get(ce);
        if (o == null) {
            o = ((DesktopCtrl) getDesktop()).getNextUuid(this);
            _ids.put(o, ce);
            _ids.put(ce, o);
        }
        return (String) o;
    }

    public CalendarEvent getCalendarEventById(String id) {
        return (CalendarEvent) _ids.get(id);
    }

    protected String getEventKey(CalendarEvent evt) {
        Date begin = evt.getBeginDate();
        Date end = evt.getEndDate();
        if (begin == null)
            throw new UiException("Begin date cannot be null: " + evt);
        if (end == null)
            throw new UiException("End date cannot be null: " + evt);
        return getEventKey(begin);
    }

    private String getEventKey(Date date) {
        return _sdfKey.format(date);
    }

    /**
     * Returns the unmodifiable list including all the calendar events matching
     * from the specified date in the same date. e.g. "20090324" exclusive the
     * time of the date "23:30".
     * <p>
     * Note: never null.
     */
    public List<CalendarEvent> getEvent(Date beginDate) {
        String key = getEventKey(beginDate);
        List<CalendarEvent> list = _evts.get(key);
        if (list != null)
            Collections.sort(list, getDefaultBeginDateComparator());
        else
            list = Collections.emptyList();
        return Collections.unmodifiableList(list);
    }

    private static final Comparator<CalendarEvent> getDefaultBeginDateComparator() {
        return _defCompare;
    }

    /**
     * Navigates the current date to the previous page, that is, when the
     * {@link #getDays()} is seven with default mold, the previous page means
     * the previous week. In the month mold, it means the previous month.
     */
    public void prev() {
        movePage(-1);
    }

    /**
     * Navigates the current date to the next page, that is, when the
     * {@link #getDays()} is seven with default mold, the next page means the
     * next week. In the month mold, it means the next month.
     */
    public void next() {
        movePage(1);
    }

    private void movePage(int day) {
        if (_curDate == null)
            return;

        Calendar cal = getCalendar();
        cal.setTime(_curDate);
        if (inMonthMold())
            cal.add(Calendar.MONTH, day);
        else
            cal.add(Calendar.DAY_OF_MONTH, day * _days);
        setCurrentDate(cal.getTime());
    }

    /**
     * Returns the beginning date, which is based on {@link #getCurrentDate()}
     * in the current view depended on which {@link #getMold()} is using.
     */
    public Date getBeginDate() {
        if (_curDate == null)
            return null;

        Calendar cal = getCalendar();
        cal.setTime(_curDate);
        boolean inMonth = inMonthMold();
        if (inMonth)
            cal.set(Calendar.DAY_OF_MONTH, 1);

        if (_days >= 7 || inMonth) { // ZKCAL-34: should consider
            // _firstDayOfWeek if week view is large
            // than 7
            int index = cal.get(Calendar.DAY_OF_WEEK);
            int offset = index - _firstDayOfWeek;
            if (offset < 0)
                offset += 7;
            cal.add(Calendar.DAY_OF_MONTH, -offset);
        }
        cal.set(Calendar.HOUR_OF_DAY, 0);
        cal.set(Calendar.MINUTE, 0);
        cal.set(Calendar.SECOND, 0);
        cal.set(Calendar.MILLISECOND, 0);

        return cal.getTime();
    }

    /**
     * Returns the end date, which is based on {@link #getCurrentDate()} in the
     * current view depended on which {@link #getMold()} is using.
     */
    public Date getEndDate() {
        Date beginDate = getBeginDate();

        if (beginDate == null)
            return null;

        Calendar cal = getCalendar();

        if (inMonthMold()) {
            int weeks = getWeekOfMonth();
            cal.setTime(_curDate);
            cal.set(Calendar.DAY_OF_MONTH, 1);
            int offset = cal.get(Calendar.DAY_OF_WEEK) - _firstDayOfWeek;
            if (offset < 0)
                offset += 7;

            cal.add(Calendar.DAY_OF_MONTH, weeks * 7 - offset);

            cal.set(Calendar.HOUR_OF_DAY, 0);
            cal.set(Calendar.MINUTE, 0);
            cal.set(Calendar.SECOND, 0);
        } else {
            cal.setTime(beginDate);
            cal.add(Calendar.DAY_OF_MONTH, _days);
        }
        cal.set(Calendar.MILLISECOND, 0);
        return cal.getTime();
    }

    /**
     * Sets the current date.
     * <p>
     * Default: today (depend on which timezone the system is using).
     */
    public void setCurrentDate(Date curDate) {
        if (curDate == null)
            throw new NullPointerException("Current Date cannot be null!");
        if (!Objects.equals(curDate, _curDate)) {
            _curDate = curDate;
            reSendDateRange();
            smartUpdate("cd", Util.getDSTTime(this.getDefaultTimeZone(),
                    getCurrentDate()));
            reSendEventGroup();
        }
    }

    /**
     * Returns the current date.
     * <p>
     * Default: today (depend on which timezone the calendar is using).
     */
    public Date getCurrentDate() {
        return _curDate;
    }

    /**
     * @since 2.1.0 Sets the begin time.
     * <p>
     * Default: 0.
     */
    public void setBeginTime(int beginTime) {
        if (0 > beginTime || beginTime > 24)
            throw new IllegalArgumentException("Illegal begin time: "
                    + beginTime + ", time range only allows from 0 to 24");
        if (beginTime > _endTime)
            throw new IllegalArgumentException("Illegal begin time: "
                    + beginTime
                    + ", begin time shall not later then end time: " + _endTime);

        if (_beginTime != beginTime) {
            _beginTime = beginTime;
            smartUpdate("bt", beginTime);
            reSendEventGroup();
        }
    }

    /**
     * @since 2.1.0 Returns the begin time.
     * <p>
     * Default: 0.
     */
    public int getBeginTime() {
        return _beginTime;
    }

    /**
     * @since 2.1.0 Sets the end time.
     * <p>
     * Default: 24.
     */
    public void setEndTime(int endTime) {
        if (0 > endTime || endTime > 24)
            throw new IllegalArgumentException("Illegal end time: " + endTime
                    + ", time range only allows from 0 to 24");
        if (_beginTime > endTime)
            throw new IllegalArgumentException("Illegal end time: " + endTime
                    + ", end time shall not early then begin time: "
                    + _beginTime);
        if (_endTime != endTime) {
            _endTime = endTime;
            smartUpdate("et", endTime);
            reSendEventGroup();
        }
    }

    /**
     * @since 2.1.0 Returns the end time.
     * <p>
     * Default: 24.
     */
    public int getEndTime() {
        return _endTime;
    }

    /**
     * @since 2.1.0 Sets the time slot per hour.
     * <p>
     * Default: 2.
     */
    public void setTimeslots(int timeslots) {
        if (timeslots != 2 && timeslots != 4 && timeslots != 6)
            throw new UiException("allowed: 2 or 4 or 6");

        if (_timeslots != timeslots) {
            this._timeslots = timeslots;
            smartUpdate("timeslots", timeslots);
            reSendEventGroup();
        }
    }

    /**
     * @since 2.1.0 Returns the time slot per hour.
     * <p>
     * Default: 2.
     */
    public int getTimeslots() {
        return _timeslots;
    }

    private void reSendDateRange() {
        reload();
        Date beginDate = getBeginDate();
        Date endDate = getEndDate();
        TimeZone timezone = getDefaultTimeZone();
        smartUpdate("bd", Util.getDSTTime(timezone, beginDate));
        smartUpdate("ed", Util.getDSTTime(timezone, endDate));
        Calendar cal = getCalendar();
        if (inMonthMold()) {
            smartUpdate("weekOfMonth", getWeekOfMonth());
            if (isWeekOfYear() && _dfmter == null) {
                cal.setTime(beginDate);
                smartUpdate("woy",
                        String.valueOf(cal.get(Calendar.WEEK_OF_YEAR)));
            }
        }

        DateFormatter dfhandler = getDateFormatter();
        if (dfhandler == null)
            return;
        final Locale locale = Locales.getCurrent();
        cal.setTime(beginDate);

        if (inMonthMold()) {
            List<List<String>> result = Util.packAllCaptionOfMonth(this, cal,
                    locale, timezone, dfhandler);
            smartUpdate("captionByDayOfWeek", Util.encloseList(result.get(0)));
            smartUpdate("captionByPopup", Util.encloseList(result.get(1)));
            smartUpdate("captionByDateOfMonth", Util.encloseList(result.get(2)));

            if (isWeekOfYear())
                smartUpdate("captionByWeekOfYear",
                        Util.encloseList(result.get(3)));
        } else
            smartUpdate("captionByDate", Util.encloseList(Util
                    .packCaptionByDate(cal, getDays(), locale, timezone,
                            dfhandler)));
    }

    /**
     * Returns the number of the week of the month in the current date.
     */
    public int getWeekOfMonth() {
        Calendar cal = getCalendar();
        // calculate how many weeks we should display
        cal.setTime(_curDate);
        int maximun = cal.getActualMaximum(Calendar.DAY_OF_MONTH);

        if (cal.getFirstDayOfWeek() != _firstDayOfWeek) {
            cal.set(Calendar.DAY_OF_MONTH, 1);
            int beginIndex = cal.get(Calendar.DAY_OF_WEEK);
            int offset = beginIndex - _firstDayOfWeek;
            if (offset < 0)
                offset += 7;

            int week = 1;
            int delta = maximun - (7 - offset);
            week += delta / 7;

            // add one for the rest days
            if (delta % 7 != 0)
                ++week;
            return week;
        } else {

            // The fixed provided by david.hamilton@jiffle.net
            cal.set(Calendar.DAY_OF_MONTH, maximun);
            int last = cal.get(Calendar.WEEK_OF_MONTH);
            cal.set(Calendar.DAY_OF_MONTH, 1);
            return last - cal.get(Calendar.WEEK_OF_MONTH) + 1;
        }
    }

    protected void reSendEventGroup() {
        if (getAttribute(ATTR_ON_INIT_POSTED) == null) {
            setAttribute(ATTR_ON_INIT_POSTED, Boolean.TRUE);
            Events.postEvent(-10100, "onInitRender", this, null);
        }
    }

    public void onInitRender() {
        removeAttribute(ATTR_ON_INIT_POSTED);
        _evts.clear();
        _ids.clear();

        final TimeZone tzone = getDefaultTimeZone();

        smartUpdate("zonesOffset",
                Util.encloseList(Util.packZonesOffset(_tzones)));
        smartUpdate("zones", Util.encloseList(_tzones.values()));
        // reset default timezone
        _sdfKey.setTimeZone(tzone);
        if (_model != null) {
            Date beginDate = getBeginDate();
            List<CalendarEvent> list = _model.get(beginDate, getEndDate(),
                    new RenderContext() {
                        public TimeZone getTimeZone() {
                            return tzone;
                        }
                    });

            if (list != null) {
                for (Iterator<CalendarEvent> it = list.iterator(); it.hasNext(); ) {
                    CalendarEvent ce = it.next();
                    if (!ce.getBeginDate().before(ce.getEndDate()))
                        throw new IllegalArgumentException(
                                "Illegal date: from " + ce.getBeginDate()
                                        + " to " + ce.getEndDate());

                    String key = ce.getBeginDate().before(beginDate) ? getEventKey(beginDate)
                            : getEventKey(ce.getBeginDate());

                    List<CalendarEvent> dayevt = _evts.get(key);
                    if (dayevt == null) {
                        dayevt = new LinkedList<CalendarEvent>();
                        _evts.put(key, dayevt);
                    }
                    dayevt.add(ce);
                }
            }
        }
        smartUpdate("events", Util.encloseEventMap(this, _evts));
    }

    public Toolbar getToolbar() {
        return (Toolbar) getFirstChild();
    }

    /**
     * Returns the calendar model.
     */
    public CalendarModel getModel() {
        return _model;
    }

    /**
     * Sets the calendar model.
     */
    public void setModel(CalendarModel model) {
        if (model != null) {
            if (_model != model) {
                if (_model != null) {
                    _model.removeCalendarDataListener(_dataListener);
                }
                _model = model;
                initDataListener();
            }
        } else if (_model != null) {
            _model.removeCalendarDataListener(_dataListener);
            _model = null;
        }
        reSendEventGroup();
    }

    /**
     * Initializes _dataListener and register the listener to the model
     */
    private void initDataListener() {
        if (_dataListener == null)
            _dataListener = new CalendarDataListener() {
                public void onChange(CalendarDataEvent event) {
                    CalendarEvent ce = event.getCalendarEvent();
                    if (ce == null) { // if large scope change, such as clearAll
                        reSendEventGroup();
                        return;
                    }
                    switch (event.getType()) {
                        case CalendarDataEvent.INTERVAL_ADDED:
                            addDayEvent(event.getCalendarEvent());
                            break;
                        case CalendarDataEvent.INTERVAL_REMOVED:
                            removeDayEvent(event.getCalendarEvent());
                            break;
                        case CalendarDataEvent.CONTENTS_CHANGED:
                            modifyDayEvent(event.getCalendarEvent());
                    }
                }
            };

        _model.addCalendarDataListener(_dataListener);
    }

    public void setMold(String mold) {
        if (mold == null || mold.length() == 0)
            mold = "default";
        if (!Objects.equals(getMold(), mold)) {
            super.setMold(mold);
            reSendEventGroup();
        }
    }

    /**
     * package
     */
    boolean inMonthMold() {
        return "month".equals(getMold()) || "year".equals(getMold());
    }

    /**
     * Sets whether the event content escape XML
     *
     * @param escapeXML
     */
    public void setEscapeXML(boolean escapeXML) {
        if (_escapeXML != escapeXML) {
            this._escapeXML = escapeXML;
            smartUpdate("escapeXML", escapeXML);
            reSendEventGroup();
        }
    }

    /**
     * Return whether the event content escape XML
     *
     * @return boolean
     */
    public boolean isEscapeXML() {
        return _escapeXML;
    }

    // super
    public String getZclass() {
        return _zclass == null ? "z-calendars" : _zclass;
    }

    // -- Component --//
    public boolean insertBefore(Component newChild, Component refChild) {
        if (newChild instanceof Toolbar) {
            Component first = getFirstChild();
            if (first instanceof Toolbar && first != newChild)
                throw new UiException("Only one toolbar child is allowed: "
                        + this);
            refChild = first;
        } else {
            throw new UiException("Unsupported child for Calendars: "
                    + newChild);
        }
        return super.insertBefore(newChild, refChild);
    }

    // Cloneable//
    public Object clone() {
        final Calendars clone = (Calendars) super.clone();
        clone.init();
        if (clone._model != null) {
            // we use the same data model but we have to create a new listener
            clone._dataListener = null;
            clone.initDataListener();
        }
        return clone;
    }

    // -- Serializable --//
    private synchronized void readObject(java.io.ObjectInputStream s)
            throws java.io.IOException, ClassNotFoundException {
        s.defaultReadObject();
        init();
        if (_model != null)
            initDataListener();
    }

    public void service(org.zkoss.zk.au.AuRequest request, boolean everError) {
        final String cmd = request.getCommand();

        if (cmd.equals(CalendarsEvent.ON_EVENT_CREATE))
            Events.postEvent(CalendarsEvent.getCreateEvent(request));
        else if (cmd.equals(CalendarsEvent.ON_EVENT_EDIT))
            Events.postEvent(CalendarsEvent.getEditEvent(request));
        else if (cmd.equals(CalendarsEvent.ON_EVENT_UPDATE))
            Events.postEvent(CalendarsEvent.getUpdateEvent(request));
        else if (cmd.equals(CalendarsEvent.ON_DAY_CLICK)
                || cmd.equals(CalendarsEvent.ON_WEEK_CLICK))
            Events.postEvent(CalendarsEvent.getClickEvent(request, cmd));
        else if (cmd.equals(Events.ON_DROP)) {
            Events.postEvent(CalendarDropEvent.getCalendarDropEvent(request));
        } else if (cmd.equals(CalendarsEvent.ON_EVENT_TOOLTIP)) {
            Events.postEvent(CalendarsEvent.getTooltipEvent(request));
        } else {
            super.service(request, everError);
        }
    }

    /*
     * (non-Javadoc)
     *
     * @see org.zkoss.zul.impl.XulElement#renderProperties(org.zkoss.web.ui.sys.
     * ContentRenderer)
     */
    protected void renderProperties(org.zkoss.zk.ui.sys.ContentRenderer renderer)
            throws IOException {
        super.renderProperties(renderer);
        TimeZone tz = getDefaultTimeZone();
        renderer.render("tz", (tz.getRawOffset()) / 60000);

        renderer.render("zonesOffset",
                Util.encloseList(Util.packZonesOffset(_tzones)));
        renderer.render("zones", Util.encloseList(_tzones.values()));

        if (inMonthMold()) {
            renderer.render("mon", true);
            renderer.render("weekOfMonth", getWeekOfMonth());

            if (_weekOfYear) {
                Calendar cal = getCalendar();
                cal.setTime(getBeginDate());
                renderer.render("woy",
                        String.valueOf(cal.get(Calendar.WEEK_OF_YEAR)));
            }
            if (_dfmter != null)
                rendererMonthData(_dfmter, renderer);
        } else {
            if (_dfmter != null)
                rendererDayData(_dfmter, renderer);
            if (_beginTime != 0)
                renderer.render("bt", _beginTime);
            if (_beginTime != 24)
                renderer.render("et", _endTime);
            if (_timeslots != 2)
                renderer.render("timeslots", _timeslots);
        }

        renderer.render("bd", Util.getDSTTime(tz, getBeginDate()));
        renderer.render("ed", Util.getDSTTime(tz, getEndDate()));
        renderer.render("cd", Util.getDSTTime(tz, getCurrentDate()));

        if (_readonly)
            renderer.render("readonly", true);

        renderer.render("days", _days);
        renderer.render("escapeXML", _escapeXML);
        renderer.render("events", Util.encloseEventMap(this, _evts));
        String pattern = ((SimpleDateFormat) DateFormat.getDateInstance(
                DateFormat.SHORT, Locales.getCurrent())).toPattern()
                .replaceAll("[^\\p{Alpha}]*y+[^\\p{Alpha}]*", "");
        renderer.render("weekFmt", pattern);
    }

    private void rendererDayData(DateFormatter dfhandler,
                                 ContentRenderer renderer) throws IOException {
        final Locale locale = Locales.getCurrent();
        Calendar cal = getCalendar();
        cal.setTime(getBeginDate());

        renderer.render("captionByDate", Util.encloseList(Util
                .packCaptionByDate(cal, getDays(), locale,
                        getDefaultTimeZone(), dfhandler)));

        cal.set(Calendar.MINUTE, 0);
        renderer.render("captionByTimeOfDay", Util.encloseList(Util
                .packCaptionByTimeOfDay(cal, _tzones, locale, dfhandler)));
    }

    private void rendererMonthData(DateFormatter dfhandler,
                                   ContentRenderer renderer) throws IOException {
        Calendar cal = getCalendar();
        cal.setTime(getBeginDate());

        List<List<String>> result = Util.packAllCaptionOfMonth(this, cal,
                Locales.getCurrent(), getDefaultTimeZone(), dfhandler);
        renderer.render("captionByDayOfWeek", Util.encloseList(result.get(0)));
        renderer.render("captionByPopup", Util.encloseList(result.get(1)));
        renderer.render("captionByDateOfMonth", Util.encloseList(result.get(2)));

        if (isWeekOfYear())
            renderer.render("captionByWeekOfYear",
                    Util.encloseList(result.get(3)));
    }

    public Object getQuery() {
        return _query;
    }

    public void setQuery(Object query) {
        this._query = query;
    }

    public String getDbId() {
        return _dbId;
    }

    public void setDbId(String dbId) {
        this._dbId = dbId;
    }

    public String getFilter() {
        return _filter;
    }

    public void setFilter(String filter) {
        this._filter = filter;
    }

    @Override
    public boolean isDisabled() {
        return _readonly;
    }

    @Override
    public void setDisabled(boolean disabled) {
        setReadonly(disabled);
    }

    public void reload() {
        Widget ext = (Widget) getAttribute("$proxy");
        if (ext != null)
            ext.reload(this);
    }
}
